1 /** 2 * PNG file format image loader/exporter 3 * 4 * License: 5 * Copyright Devisualization (Richard Andrew Cattermole) 2014 - 2017. 6 * Distributed under the Boost Software License, Version 1.0. 7 * (See accompanying file LICENSE_1_0.txt or copy at 8 * http://www.boost.org/LICENSE_1_0.txt) 9 */ 10 module devisualization.image.fileformats.png; 11 import devisualization.image.fileformats.defs : HeadersOnly, ImageNotLoadableException, ImageNotExportableException; 12 import devisualization.image.interfaces; 13 import devisualization.image.primitives : isImage, ImageColor; 14 import devisualization.image.storage.base : ImageStorageHorizontal; 15 import devisualization.util.core.memory.managed; 16 import stdx.allocator : IAllocator, theAllocator, makeArray, make, expandArray, dispose; 17 import std.experimental.color : isColor, RGB8, RGBA8, convertColor; 18 import std.experimental.color.rgb : RGB; 19 import std.range : isInputRange, ElementType; 20 import std.datetime : DateTime; 21 import std.traits : isPointer; 22 import std.typecons : tuple; 23 24 /// 25 alias HeadersOnlyPNGFileFormat = PNGFileFormat!HeadersOnly; 26 27 /** 28 * PNG file format representation 29 * 30 * Does not actually support color correction/alteration for the chunks: cHRM, sRGB, iCCP and gAMA. 31 * Use them once you have already performed the alterations upon IDAT and respective data. 32 * 33 * FIXME: 34 * Reliance on e.g. GC/processAllocator for when compressing/decompressing via zlib. 35 * Exporters use of filters 2, 3 and 4. Creates artifacts. 36 */ 37 struct PNGFileFormat(Color) if (isColor!Color || is(Color == HeadersOnly)) { 38 import std.bitmanip : bigEndianToNative, nativeToBigEndian; 39 import containers.hashmap; 40 import containers.dynamicarray; 41 42 /// 43 IHDR_Chunk IHDR; 44 45 /// 46 PLTE_Chunk* PLTE; 47 48 /// 49 tRNS_Chunk* tRNS; 50 51 /// 52 gAMA_Chunk* gAMA; 53 54 /// 55 cHRM_Chunk* cHRM; 56 57 /// 58 sRGB_Chunk* sRGB; 59 60 /// 61 iCCP_Chunk* iCCP; 62 63 /// tEXt values are really latin-1 but treated as UTF-8 in D code, may originate from iEXt 64 HashMap!(PngTextKeywords, string, IAllocator)* tEXt; 65 /// zEXt values are really latin-1 but treated as UTF-8 in D code, may originate from iEXt 66 HashMap!(PngTextKeywords, string, IAllocator)* zEXt; 67 68 /// 69 bKGD_Chunk* bKGD; 70 71 /// 72 pPHs_Chunk* pPHs; 73 74 /// 75 sBIT_Chunk* sBIT; 76 77 /// 78 DynamicArray!(sPLT_Chunk, IAllocator)* sPLT; 79 /// 80 DynamicArray!(ushort, IAllocator)* hIST; 81 82 /// 83 DateTime* tIME; 84 85 // we can't copy because of ImageStorage type probably won't be able to be 86 @disable 87 this(this); 88 89 static if (!is(Color == HeadersOnly)) { 90 /// Only available when Color is specified as not HeadersOnly 91 ImageStorage!Color value; 92 alias value this; 93 94 /// 95 managed!(ubyte[]) toBytes() { 96 return performExport(); 97 } 98 } 99 100 string toString() { 101 import std.conv : text; 102 char[] ret; 103 104 void addText(string[] toAdd...) { 105 import std.algorithm : sum, map; 106 size_t len = ret.length; 107 108 if (len == 0) 109 ret = allocator.makeArray!char(toAdd.map!`a.length`.sum); 110 else 111 allocator.expandArray(ret, toAdd.map!`a.length`.sum); 112 113 foreach(v; toAdd) { 114 ret[len .. len + v.length] = v[]; 115 len += v.length; 116 } 117 } 118 119 addText("PNGFileFormat!", Color.stringof, " [\n\tChunks: [\n"); 120 121 addText("\t\t", IHDR.text, ",\n"); 122 123 if (tEXt.keys.length > 0) { 124 addText("\t\ttEXt(\n"); 125 foreach(const(PngTextKeywords) k, string v; *tEXt) { 126 addText("\t\t\t[", k, "]: {", v, "}\n"); 127 } 128 addText("\t\t)\n"); 129 } 130 if (zEXt.keys.length > 0) { 131 addText("\t\tzEXt(\n"); 132 foreach(const(PngTextKeywords) k, string v; *zEXt) { 133 addText("\t\t\t[", k, "]: {", v, "}\n"); 134 } 135 addText("\t\t)\n"); 136 } 137 138 if (PLTE !is null) 139 addText("\t\t", (*PLTE).text, ",\n"); 140 if (tRNS !is null) 141 addText("\t\t", (*tRNS).text, ",\n"); 142 if (gAMA !is null) 143 addText("\t\t", (*gAMA).text, ",\n"); 144 if (cHRM !is null) 145 addText("\t\t", (*cHRM).text, ",\n"); 146 if (sRGB !is null) 147 addText("\t\t", (*sRGB).text, ",\n"); 148 if (iCCP !is null) 149 addText("\t\t", (*iCCP).text, ",\n"); 150 if (bKGD !is null) 151 addText("\t\t", (*bKGD).text, ",\n"); 152 if (pPHs !is null) 153 addText("\t\t", (*pPHs).text, ",\n"); 154 if (sBIT !is null) 155 addText("\t\t", (*sBIT).text, ",\n"); 156 if (tIME !is null) 157 addText("\t\t", (*tIME).text, ",\n"); 158 159 if (sPLT.length > 0) 160 addText("\t\t", (*sPLT)[].text, ",\n"); 161 if (hIST.length > 0) 162 addText("\t\t", (*hIST)[].text, ",\n"); 163 164 addText("\t]\n"); 165 static if (!is(Color == HeadersOnly)) { 166 addText("\tData: [\n"); 167 168 foreach(y; 0 .. value.height) { 169 addText("\t\t(y: ", y.text, " "); 170 foreach(x; 0 .. value.width) { 171 addText("(x: ", x.text, " ", value.getPixel(x, y).text, ")"); // FIXME: text allocates 172 if (x + 1 < value.width) 173 addText(", "); 174 } 175 addText(")"); 176 if (y + 1 < value.height) 177 addText(",\n"); 178 else 179 addText("\n"); 180 } 181 182 addText("\t]\n"); 183 } 184 185 addText("]"); 186 return cast(string)ret; 187 } 188 189 @property { 190 /// 191 IAllocator allocator() { 192 return alloc; 193 } 194 } 195 196 this(IAllocator allocator) { 197 this.alloc = allocator; 198 tEXt = allocator.make!(HashMap!(PngTextKeywords, string, IAllocator))(allocator); 199 zEXt = allocator.make!(HashMap!(PngTextKeywords, string, IAllocator))(allocator); 200 sPLT = allocator.make!(DynamicArray!(sPLT_Chunk, IAllocator))(allocator); 201 hIST = allocator.make!(DynamicArray!(ushort, IAllocator))(allocator); 202 } 203 204 ~this() { 205 if (allocator is null) return; 206 207 if (PLTE !is null) 208 allocator.dispose(PLTE); 209 if (tRNS !is null) 210 allocator.dispose(tRNS); 211 if (gAMA !is null) 212 allocator.dispose(gAMA); 213 if (cHRM !is null) 214 allocator.dispose(cHRM); 215 if (sRGB !is null) 216 allocator.dispose(sRGB); 217 if (iCCP !is null) 218 allocator.dispose(iCCP); 219 if (bKGD !is null) 220 allocator.dispose(bKGD); 221 if (pPHs !is null) 222 allocator.dispose(pPHs); 223 if (sBIT !is null) 224 allocator.dispose(sBIT); 225 if (tIME !is null) 226 allocator.dispose(tIME); 227 228 static if (!is(Color == HeadersOnly)) { 229 if (IDAT !is null) { 230 allocator.dispose(IDAT.data); 231 allocator.dispose(IDAT); 232 } 233 if (value !is null) { 234 allocator.dispose(value); 235 } 236 } 237 } 238 239 private { 240 IAllocator alloc; 241 242 void delegate(size_t width, size_t height) @trusted theImageAllocator; 243 244 static if (!is(Color == HeadersOnly)) { 245 IDAT_Chunk!Color* IDAT; 246 247 void allocateTheImage(ImageImpl)(size_t width, size_t height) @trusted { 248 static if (is(ImageImpl : ImageStorage!Color)) { 249 value = alloc.make!(ImageImpl)(width, height, alloc); 250 } else { 251 value = imageObject!(ImageImpl)(width, height, alloc); 252 } 253 } 254 } else { 255 // this gets checked for so many places, that it would be a pain to actually fix it there. 256 // instead declare it as an untyped pointer to emulate 'is null'. 257 // it should _never_ be assigned to! 258 void* IDAT = null; 259 } 260 261 /* 262 * The importer 263 */ 264 265 void performInput(IR)(IR input) @trusted { 266 import std.range; 267 ubyte[] buffer = allocator.makeArray!ubyte(1024 * 1024 * 8); // 8mb 268 269 ubyte popReadValue() { 270 if (input.empty) 271 throw new ImageNotLoadableException("Input was not long enough"); 272 273 ubyte ret = input.front; 274 input.popFront; 275 276 return ret; 277 } 278 279 bool checkHasPNGText() { 280 if (!popReadValue == 0x89) 281 return false; 282 if (!popReadValue == 0x50) 283 return false; 284 if (!popReadValue == 0x4E) 285 return false; 286 if (!popReadValue == 0x47) 287 return false; 288 if (!popReadValue == 0x0D) 289 return false; 290 if (!popReadValue == 0x0A) 291 return false; 292 if (!popReadValue == 0x1A) 293 return false; 294 if (!popReadValue == 0x0A) 295 return false; 296 return true; 297 } 298 if (!checkHasPNGText()) { 299 throw new ImageNotLoadableException("Input was not a PNG image"); 300 } 301 302 ubyte[] readChunk(out char[4] name) { 303 import std.digest.crc : crc32Of, crcHexString; 304 import std.conv : to; 305 ubyte[4] tuintbuff; 306 307 // chunk length 308 tuintbuff[0] = popReadValue; 309 tuintbuff[1] = popReadValue; 310 tuintbuff[2] = popReadValue; 311 tuintbuff[3] = popReadValue; 312 uint chunkLength = bigEndianToNative!uint(tuintbuff); 313 314 // chunk name 315 name[0] = popReadValue; 316 name[1] = popReadValue; 317 name[2] = popReadValue; 318 name[3] = popReadValue; 319 320 // chunk data 321 if (buffer.length < chunkLength + 4) 322 allocator.expandArray(buffer, chunkLength + 4); 323 324 buffer[0 .. 4] = cast(ubyte[4])name[]; 325 foreach(index; 4 .. chunkLength + 4) { 326 buffer[index] = popReadValue; 327 } 328 329 // get the CRC code for chunk 330 tuintbuff[0] = popReadValue; 331 tuintbuff[1] = popReadValue; 332 tuintbuff[2] = popReadValue; 333 tuintbuff[3] = popReadValue; 334 uint crcCode = bigEndianToNative!uint(tuintbuff); 335 336 // check the chunk has not been "tampered" with 337 // FIXME: probably allocated in crcHexString ew 338 if (crcHexString(crc32Of(buffer[0 .. chunkLength + 4])) != crcHexString((cast(ubyte*)&crcCode)[0 .. 4])) { 339 throw allocator.make!ImageNotLoadableException("CRC code did not match for chunk"); 340 } 341 342 return buffer[4 .. chunkLength + 4]; 343 } 344 345 WL: while(!input.empty) { 346 char[4] chunkName; 347 ubyte[] chunkData = readChunk(chunkName); 348 349 switch(chunkName) { 350 case "IHDR": 351 readChunk_IHDR(chunkData); 352 break; 353 case "cHRM": 354 // preceede IDAT, PLTE _only_ 355 if (IDAT !is null || PLTE !is null) 356 throw allocator.make!ImageNotLoadableException("cHRM chunk must preceede IDAT or PLTE chunks"); 357 else if (cHRM !is null) // must not have a iCCP chunk as well 358 throw allocator.make!ImageNotLoadableException("Only one cHRM chunk can exist"); 359 360 readChunk_cHRM(chunkData); 361 break; 362 case "sRGB": 363 // preceede IDAT, PLTE _only_ 364 if (IDAT !is null || PLTE !is null) 365 throw allocator.make!ImageNotLoadableException("sRGB chunk must preceede IDAT or PLTE chunks"); 366 else if (iCCP !is null) // must not have a iCCP chunk as well 367 throw allocator.make!ImageNotLoadableException("sRGB chunk must not exist along with iCCP chunk"); 368 else if (sRGB !is null) // must not have a sRGB chunk as well 369 throw allocator.make!ImageNotLoadableException("Only one sRGB chunk can exist"); 370 371 readChunk_sRGB(chunkData); 372 break; 373 case "iCCP": 374 // preceede IDAT, PLTE _only_ 375 if (IDAT !is null || PLTE !is null) 376 throw allocator.make!ImageNotLoadableException("iCCP chunk must preceede IDAT or PLTE chunks"); 377 else if (sRGB !is null) // must not have a sRGB chunk as well 378 throw allocator.make!ImageNotLoadableException("iCCP chunk must not exist along with sRGB chunk"); 379 else if (iCCP !is null) // must not have a iCCP chunk as well 380 throw allocator.make!ImageNotLoadableException("Only one iCCP chunk can exist"); 381 382 readChunk_iCCP(chunkData); 383 break; 384 case "PLTE": 385 if (IDAT !is null || bKGD !is null) 386 throw allocator.make!ImageNotLoadableException("iCCP chunk must preceede IDAT and bKGD chunk(s)"); 387 else if (PLTE !is null) // must not have a PLTE chunk as well 388 throw allocator.make!ImageNotLoadableException("Only one PLTE chunk can exist"); 389 390 readChunk_PLTE(chunkData); 391 break; 392 case "gAMA": 393 if (IDAT !is null || PLTE !is null) 394 throw allocator.make!ImageNotLoadableException("gAMA chunk must preceede IDAT or PLTE chunks"); 395 else if (gAMA !is null) // must not have a gAMA chunk as well 396 throw allocator.make!ImageNotLoadableException("Only one gAMA chunk can exist"); 397 398 readChunk_gAMA(chunkData); 399 break; 400 case "tRNS": 401 if (IDAT !is null || PLTE !is null) 402 throw allocator.make!ImageNotLoadableException("tRNS chunk must preceede IDAT or PLTE chunks"); 403 else if (tRNS !is null) // must not have a tRNS chunk as well 404 throw allocator.make!ImageNotLoadableException("Only one tRNS chunk can exist"); 405 406 readChunk_tRNS(chunkData); 407 break; 408 case "tEXt": 409 readChunk_tEXt(chunkData); 410 break; 411 case "zEXt": 412 readChunk_zEXt(chunkData); 413 break; 414 case "iTXt": 415 readChunk_iEXt(chunkData); 416 break; 417 case "bKGD": 418 if (IDAT !is null) 419 throw allocator.make!ImageNotLoadableException("bKGD chunk must preceede IDAT chunks"); 420 else if (bKGD !is null) // must not have a tRNS chunk as well 421 throw allocator.make!ImageNotLoadableException("Only one bKGD chunk can exist"); 422 423 readChunk_bKGD(chunkData); 424 break; 425 case "pPHs": 426 if (IDAT !is null) 427 throw allocator.make!ImageNotLoadableException("pPHs chunk must preceede IDAT chunks"); 428 else if (pPHs !is null) // must not have a tRNS chunk as well 429 throw allocator.make!ImageNotLoadableException("Only one pPHs chunk can exist"); 430 431 readChunk_pPHs(chunkData); 432 break; 433 case "sBIT": 434 if (IDAT !is null) 435 throw allocator.make!ImageNotLoadableException("sBIT chunk must preceede IDAT chunks"); 436 else if (sBIT !is null) // must not have a tRNS chunk as well 437 throw allocator.make!ImageNotLoadableException("Only one sBIT chunk can exist"); 438 439 readChunk_sBIT(chunkData); 440 break; 441 case "sPLT": 442 if (IDAT !is null) 443 throw allocator.make!ImageNotLoadableException("sPLT chunk must preceede IDAT chunks"); 444 445 readChunk_sPLT(chunkData); 446 break; 447 case "hIST": 448 if (IDAT !is null) 449 throw allocator.make!ImageNotLoadableException("hIST chunk must preceede IDAT chunks"); 450 else if (PLTE !is null) 451 throw allocator.make!ImageNotLoadableException("hIST chunk must proceed PLTE chunk"); 452 453 readChunk_hIST(chunkData); 454 break; 455 456 case "tIME": 457 readChunk_tIME(chunkData); 458 break; 459 460 static if (!is(Color == HeadersOnly)) { 461 case "IDAT": 462 if (hIST.length > 0 && PLTE.colors.length != hIST.length) 463 throw allocator.make!ImageNotLoadableException("hIST and PLTE chunks must have the same index length"); 464 if ((IHDR.colorType & PngIHDRColorType.Palette) == PngIHDRColorType.Palette && tRNS !is null && tRNS.indexAlphas.length < PLTE.colors.length) 465 allocator.expandArray(tRNS.indexAlphas, tRNS.indexAlphas.length - PLTE.colors.length, 255); 466 467 if (IDAT is null) {// allocate the image storage 468 IDAT = allocator.make!(IDAT_Chunk!Color); 469 IDAT.data = allocator.makeArray!ubyte(chunkData.length); 470 theImageAllocator(IHDR.width, IHDR.height); 471 } else 472 allocator.expandArray(IDAT.data, chunkData.length); 473 IDAT.data[$-chunkData.length .. $] = chunkData[]; 474 break; 475 } 476 477 case "IEND": 478 static if (!is(Color == HeadersOnly)) { 479 if (IDAT is null) 480 throw allocator.make!ImageNotLoadableException("No IDAT chunk present"); 481 482 readChunk_IDAT(IDAT.data); 483 } 484 485 readChunk_IEND(chunkData); 486 break WL; 487 default: 488 break; 489 } 490 } 491 492 allocator.dispose(buffer); 493 } 494 495 void readChunk_IHDR(ubyte[] chunkData) @trusted { 496 if (chunkData.length != 13) 497 throw allocator.make!ImageNotLoadableException("IHDR chunk size must be 13 bytes long"); 498 499 IHDR = IHDR_Chunk( 500 bigEndianToNative!uint(cast(ubyte[4])chunkData[0 .. 4]), 501 bigEndianToNative!uint(cast(ubyte[4])chunkData[4 .. 8]), 502 cast(PngIHDRBitDepth) chunkData[8], 503 cast(PngIHDRColorType) chunkData[9], 504 cast(PngIHDRCompresion) chunkData[10], 505 cast(PngIHDRFilter) chunkData[11], 506 cast(PngIHDRInterlaceMethod) chunkData[12]); 507 } 508 509 void readChunk_cHRM(ubyte[] chunkData) @trusted { 510 if (chunkData.length != 32) 511 throw allocator.make!ImageNotLoadableException("cHRM chunk must be 32 bytes long"); 512 513 cHRM = allocator.make!cHRM_Chunk( 514 bigEndianToNative!uint(cast(ubyte[4])chunkData[0 .. 4]), 515 bigEndianToNative!uint(cast(ubyte[4])chunkData[4 .. 8]), 516 bigEndianToNative!uint(cast(ubyte[4])chunkData[8 .. 12]), 517 bigEndianToNative!uint(cast(ubyte[4])chunkData[12 .. 16]), 518 bigEndianToNative!uint(cast(ubyte[4])chunkData[16 .. 20]), 519 bigEndianToNative!uint(cast(ubyte[4])chunkData[20 .. 24]), 520 bigEndianToNative!uint(cast(ubyte[4])chunkData[24 .. 28]), 521 bigEndianToNative!uint(cast(ubyte[4])chunkData[28 .. 32])); 522 } 523 524 void readChunk_sRGB(ubyte[] chunkData) @trusted { 525 if (chunkData.length != 1) 526 throw allocator.make!ImageNotLoadableException("cHRM chunk must be 1 byte long"); 527 528 sRGB = allocator.make!sRGB_Chunk(cast(PngRenderingIntent)chunkData[0]); 529 } 530 531 void readChunk_iCCP(ubyte[] chunkData) @trusted { 532 import std.zlib : uncompress; 533 534 char[] profileName; 535 foreach(i, c; chunkData) { 536 if (i >= 80) 537 throw allocator.make!ImageNotLoadableException("iCCP chunk profile name must be less then 80 characters long (c-string\\0)"); 538 539 if (c == 0) {// null terminator 540 if (i == 0) // error 541 throw allocator.make!ImageNotLoadableException("iCCP chunk profile name must be atleast 1 character in length"); 542 543 profileName = cast(char[])chunkData[0 .. i]; 544 545 if (i + 2 < chunkData.length) 546 throw allocator.make!ImageNotLoadableException("iCCP chunk data is not long enough"); 547 chunkData = chunkData[i + 1 .. $]; 548 break; 549 } 550 } 551 552 iCCP = allocator.make!iCCP_Chunk(); 553 554 // profile name 555 iCCP.profileName = cast(string)allocator.makeArray!char(profileName.length); 556 cast(char[])iCCP.profileName[] = profileName[]; 557 558 // compression method deflate/inflate 559 iCCP.compressionMethod = cast(PngIHDRCompresion)chunkData[0]; 560 561 // 562 if (iCCP.compressionMethod == PngIHDRCompresion.DeflateInflate) { 563 iCCP.profile = cast(ubyte[])uncompress(chunkData[1 .. $]); 564 } else { 565 throw allocator.make!ImageNotLoadableException("Unknown iCCP chunk compression method"); 566 } 567 } 568 569 void readChunk_PLTE(ubyte[] chunkData) @trusted { 570 PLTE = allocator.make!PLTE_Chunk; 571 572 if ((chunkData.length % 3) > 0) 573 throw allocator.make!ImageNotLoadableException("PLTE chunk size must be devisible by 3"); 574 else if ((chunkData.length / 3) > 256) 575 throw allocator.make!ImageNotLoadableException("PLTE chunk must contain at the most 256 entries"); 576 577 PLTE.colors = allocator.makeArray!(PLTE_Chunk.Color)(chunkData.length / 3); 578 579 size_t offset; 580 for (size_t i; i < (chunkData.length / 3); i++) { 581 PLTE.colors[i] = PLTE_Chunk.Color(chunkData[offset], chunkData[offset + 1], chunkData[offset + 2]); 582 offset += 3; 583 } 584 } 585 586 void readChunk_gAMA(ubyte[] chunkData) @trusted { 587 if (chunkData.length != 4) 588 throw allocator.make!ImageNotLoadableException("gAMA chunk must be 4 bytes in size"); 589 590 gAMA = allocator.make!gAMA_Chunk(bigEndianToNative!uint(cast(ubyte[4])chunkData[0 .. 4])); 591 } 592 593 void readChunk_tRNS(ubyte[] chunkData) @trusted { 594 import std.range : inputRangeObject; 595 tRNS = allocator.make!tRNS_Chunk(); 596 597 if (IHDR.colorType & PngIHDRColorType.Palette) { 598 tRNS.indexAlphas = allocator.makeArray!ubyte(chunkData.length); 599 tRNS.indexAlphas[] = chunkData[]; 600 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 601 if (chunkData.length != 2) 602 throw allocator.make!ImageNotLoadableException("tRNS chunk size must be 2 bytes when it is grayscale"); 603 604 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 605 ushort c = bigEndianToNative!ushort(cast(ubyte[2])chunkData[0 .. 2]); 606 tRNS.b16 = RGB16(c, c, c); 607 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 608 // TODO: confirm that it is the first byte and not the second? 609 tRNS.b8 = RGB8(chunkData[0], chunkData[0], chunkData[0]); 610 } else { 611 ubyte c = cast(ubyte)(chunkData[0] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 612 tRNS.b8 = RGB8(c, c, c); 613 } 614 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 615 if (chunkData.length != 3) 616 throw allocator.make!ImageNotLoadableException("tRNS chunk size must be 2 bytes when it is grayscale"); 617 618 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 619 ushort r = bigEndianToNative!ushort(cast(ubyte[2])chunkData[0 .. 2]); 620 ushort g = bigEndianToNative!ushort(cast(ubyte[2])chunkData[2 .. 4]); 621 ushort b = bigEndianToNative!ushort(cast(ubyte[2])chunkData[4 .. 6]); 622 tRNS.b16 = RGB16(r, g, b); 623 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 624 // TODO: confirm that it is the first byte and not the second? 625 tRNS.b8 = RGB8(chunkData[0], chunkData[2], chunkData[4]); 626 } else { 627 tRNS.b8 = RGB8(cast(ubyte)(chunkData[0] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)), 628 cast(ubyte)(chunkData[2] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)), 629 cast(ubyte)(chunkData[4] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1))); 630 } 631 } 632 } 633 634 void readChunk_tEXt(ubyte[] chunkData) @trusted { 635 size_t sinceLast; 636 ubyte[] buffer; 637 ubyte[] keyword; 638 639 if (chunkData.length < 2) 640 throw allocator.make!ImageNotLoadableException("tEXt chunk must be atleast 2 bytes"); 641 642 foreach(i, c; chunkData) { 643 if (c == 0) { 644 keyword = buffer; 645 buffer = null; 646 sinceLast = i + 1; 647 } else { 648 buffer = chunkData[sinceLast .. i + 1]; 649 } 650 } 651 652 char[] keyword2 = allocator.makeArray!char(keyword.length); 653 keyword2[] = cast(char[])keyword[]; 654 char[] buffer2 = allocator.makeArray!char(buffer.length); 655 buffer2[] = cast(char[])buffer[]; 656 657 (*tEXt)[cast(PngTextKeywords)keyword2] = cast(string)buffer2; 658 } 659 660 void readChunk_zEXt(ubyte[] chunkData) @trusted { 661 import std.zlib : uncompress; 662 663 size_t sinceLast; 664 ubyte[] buffer; 665 ubyte[] keyword; 666 667 if (chunkData.length < 2) 668 throw allocator.make!ImageNotLoadableException("zEXt chunk must be atleast 2 bytes"); 669 670 foreach(i, c; chunkData) { 671 if (c == 0) { 672 keyword = buffer; 673 buffer = null; 674 sinceLast = i + 1; 675 } else { 676 buffer = chunkData[sinceLast .. i + 1]; 677 } 678 } 679 680 char[] keyword2 = allocator.makeArray!char(keyword.length); 681 keyword2[] = cast(char[])keyword[]; 682 683 if (buffer.length > 0) 684 throw allocator.make!ImageNotLoadableException("zEXt chunk must have a compression method"); 685 686 ubyte compressionMethod = buffer[0]; 687 if (compressionMethod > 0) 688 throw allocator.make!ImageNotLoadableException("zEXt chunk unknown compression method"); 689 690 buffer = cast(ubyte[])uncompress(buffer); 691 692 char[] buffer2 = allocator.makeArray!char(buffer.length - 1); 693 buffer2[] = cast(char[])buffer[1 .. $]; 694 695 (*zEXt)[cast(PngTextKeywords)keyword2] = cast(string)buffer2; 696 } 697 698 void readChunk_iEXt(ubyte[] chunkData) @trusted { 699 import std.zlib : uncompress; 700 701 size_t sinceLast; 702 ubyte[] buffer; 703 ubyte[] keyword; 704 705 if (chunkData.length < 2) 706 throw allocator.make!ImageNotLoadableException("iEXt chunk must be atleast 2 bytes"); 707 708 foreach(i, c; chunkData) { 709 if (c == 0 && keyword !is null) { 710 //keyword = buffer; 711 buffer = null; 712 sinceLast = i + 1; 713 } else { 714 buffer = chunkData[sinceLast .. i + 1]; 715 } 716 } 717 718 if (buffer.length > 0) 719 throw allocator.make!ImageNotLoadableException("iEXt chunk must have a compression method"); 720 bool useCompression = cast(bool)buffer[0]; 721 ubyte compressionMethod = buffer[1]; 722 if (useCompression && compressionMethod > 0) 723 throw allocator.make!ImageNotLoadableException("iEXt chunk unknown compression method"); 724 buffer = buffer[1 .. $]; 725 726 if (buffer.length > 0) 727 throw allocator.make!ImageNotLoadableException("iEXt chunk must have a language"); 728 729 ubyte[] buffer2; 730 ubyte[] language; 731 foreach(i, c; buffer) { 732 if (c == 0 && language !is null) { 733 language = buffer2; 734 buffer2 = null; 735 sinceLast = i + 1; 736 } else { 737 buffer2 = buffer[sinceLast .. i + 1]; 738 } 739 } 740 buffer = buffer2; 741 742 if (buffer.length > 0) 743 throw allocator.make!ImageNotLoadableException("iEXt chunk must have a translated keyword"); 744 745 foreach(i, c; buffer) { 746 if (c == 0 && language !is null) { 747 keyword = buffer2; 748 buffer2 = null; 749 sinceLast = i + 1; 750 } else { 751 buffer2 = buffer[sinceLast .. i + 1]; 752 } 753 } 754 buffer = buffer2; 755 756 if (useCompression && compressionMethod == 0) 757 buffer = cast(ubyte[])uncompress(buffer); 758 759 char[] keyword2 = allocator.makeArray!char(keyword.length); 760 keyword2[] = cast(char[])keyword[]; 761 762 char[] buffer3 = allocator.makeArray!char(buffer.length); 763 buffer3 = cast(char[])buffer[]; 764 765 if (useCompression) 766 (*zEXt)[cast(PngTextKeywords)keyword2] = cast(string)buffer3; 767 else 768 (*tEXt)[cast(PngTextKeywords)keyword2] = cast(string)buffer3; 769 } 770 771 void readChunk_bKGD(ubyte[] chunkData) @trusted { 772 bKGD = allocator.make!bKGD_Chunk(); 773 774 if (IHDR.colorType & PngIHDRColorType.Palette) { 775 if (chunkData.length != 2) 776 throw allocator.make!ImageNotLoadableException("bKGD chunk size must be 1 bytes when it is palette"); 777 778 bKGD.index = chunkData[0]; 779 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 780 if (chunkData.length != 2) 781 throw allocator.make!ImageNotLoadableException("bKGD chunk size must be 2 bytes when it is grayscale"); 782 783 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 784 ushort c = bigEndianToNative!ushort(cast(ubyte[2])chunkData[0 .. 2]); 785 bKGD.b16 = RGB16(c, c, c); 786 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 787 // TODO: confirm that it is the first byte and not the second? 788 bKGD.b8 = RGB8(chunkData[0], chunkData[0], chunkData[0]); 789 } else { 790 ubyte c = cast(ubyte)(chunkData[0] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 791 bKGD.b8 = RGB8(c, c, c); 792 } 793 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 794 if (chunkData.length != 3) 795 throw allocator.make!ImageNotLoadableException("bKGD chunk size must be 2 bytes when it is grayscale"); 796 797 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 798 ushort r = bigEndianToNative!ushort(cast(ubyte[2])chunkData[0 .. 2]); 799 ushort g = bigEndianToNative!ushort(cast(ubyte[2])chunkData[2 .. 4]); 800 ushort b = bigEndianToNative!ushort(cast(ubyte[2])chunkData[4 .. 6]); 801 bKGD.b16 = RGB16(r, g, b); 802 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 803 // TODO: confirm that it is the first byte and not the second? 804 bKGD.b8 = RGB8(chunkData[0], chunkData[2], chunkData[4]); 805 } else { 806 bKGD.b8 = RGB8(cast(ubyte)(chunkData[0] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)), 807 cast(ubyte)(chunkData[2] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)), 808 cast(ubyte)(chunkData[4] * cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1))); 809 } 810 } 811 } 812 813 void readChunk_pPHs(ubyte[] chunkData) @trusted { 814 pPHs = allocator.make!pPHs_Chunk(); 815 if (chunkData.length != 9) 816 throw allocator.make!ImageNotLoadableException("pPHs chunk size must be 9 bytes"); 817 818 pPHs.ppx = bigEndianToNative!uint(cast(ubyte[4])chunkData[0 .. 4]); 819 pPHs.ppy = bigEndianToNative!uint(cast(ubyte[4])chunkData[4 .. 8]); 820 pPHs.unit = cast(PngPhysicalPixelUnit)chunkData[8]; 821 } 822 823 void readChunk_sBIT(ubyte[] chunkData) @trusted { 824 sBIT = allocator.make!sBIT_Chunk(); 825 826 if (IHDR.colorType & PngIHDRColorType.Palette) { 827 if (chunkData.length != 3) 828 throw allocator.make!ImageNotLoadableException("sBIT chunk size must be 3 byte for palette color type"); 829 830 sBIT.indexed[] = chunkData[0 .. 3]; 831 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 832 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 833 834 if ((!withAlpha && chunkData.length != 1) || (withAlpha && chunkData.length != 2)) 835 throw allocator.make!ImageNotLoadableException("sBIT chunk size must be 1 byte for grayscale color type and 2 for grayscale with alpha"); 836 837 if (withAlpha) 838 sBIT.grayScaleAlpha[] = chunkData[0 .. 2]; 839 else 840 sBIT.grayScale = chunkData[0]; 841 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 842 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 843 844 if ((!withAlpha && chunkData.length != 3) || (withAlpha && chunkData.length != 4)) 845 throw allocator.make!ImageNotLoadableException("sBIT chunk size must be 3 bytes for truecolor color type and 4 for truecolor with alpha"); 846 847 if (withAlpha) 848 sBIT.trueColorAlpha[] = chunkData[0 .. 4]; 849 else 850 sBIT.trueColor[] = chunkData[0 .. 3]; 851 } 852 } 853 854 void readChunk_sPLT(ubyte[] chunkData) @trusted { 855 sPLT_Chunk chunk; 856 857 if (chunkData.length < 2) 858 throw allocator.make!ImageNotLoadableException("sPLT chunk size must greater than 2 bytes"); 859 860 ubyte[] buffer; 861 char[] name; 862 size_t sinceLast; 863 864 foreach(i, c; chunkData) { 865 if (c == 0 && name !is null) { 866 name = cast(char[])buffer; 867 buffer = null; 868 sinceLast = i + 1; 869 } else { 870 buffer = buffer[sinceLast .. i + 1]; 871 } 872 } 873 874 chunk.paletteName = cast(string)allocator.makeArray!char(buffer.length); 875 (cast(char[])chunk.paletteName)[] = name; 876 877 if (buffer.length < 2) 878 throw allocator.make!ImageNotLoadableException("sPLT chunk size must be greater than 1 byte for sample depth"); 879 880 chunk.sampleDepth = cast(PngIHDRBitDepth)buffer[0]; 881 buffer = buffer[1 .. $]; 882 883 size_t count; 884 if (chunk.sampleDepth == PngIHDRBitDepth.BitDepth8) { 885 if (buffer.length % 6 == 0) { 886 chunk.colors = allocator.makeArray!(sPLT_Chunk.Entry)(buffer.length / 6); 887 888 for(size_t i; i < buffer.length; i += 6) { 889 ubyte[2] values; 890 values[] = chunkData[i + 4 .. i + 6]; 891 892 chunk.colors[count].color.b8 = RGBA8(chunkData[i], chunkData[i + 1], chunkData[i + 2], chunkData[i + 3]); 893 chunk.colors[count].frequency = bigEndianToNative!ushort(values); 894 count++; 895 } 896 } else { 897 throw allocator.make!ImageNotLoadableException("sPLT chunk palette must be devisible by 6 for sample depth of 8"); 898 } 899 } else if (chunk.sampleDepth == PngIHDRBitDepth.BitDepth16) { 900 if (buffer.length % 10 == 0) { 901 chunk.colors = allocator.makeArray!(sPLT_Chunk.Entry)(buffer.length / 10); 902 903 for(size_t i; i < buffer.length; i += 10) { 904 ubyte[2][5] values; 905 values[0][] = chunkData[i .. i + 2]; 906 values[1][] = chunkData[i + 2 .. i + 4]; 907 values[2][] = chunkData[i + 4 .. i + 6]; 908 values[3][] = chunkData[i + 6 .. i + 8]; 909 values[4][] = chunkData[i + 8 .. i + 10]; 910 911 chunk.colors[count].color.b16 = RGBA16( 912 bigEndianToNative!ushort(values[0]), 913 bigEndianToNative!ushort(values[1]), 914 bigEndianToNative!ushort(values[2]), 915 bigEndianToNative!ushort(values[3])); 916 chunk.colors[count].frequency = bigEndianToNative!ushort(values[4]); 917 count++; 918 } 919 } else { 920 throw allocator.make!ImageNotLoadableException("sPLT chunk palette must be devisible by 10 for sample depth of 16"); 921 } 922 } else { 923 throw allocator.make!ImageNotLoadableException("sPLT chunk must have a bit depth of either 8 or 16"); 924 } 925 926 *sPLT ~= chunk; 927 } 928 929 void readChunk_hIST(ubyte[] chunkData) @trusted { 930 if (chunkData.length % 2 == 1) 931 throw allocator.make!ImageNotLoadableException("hIST chunk must be devisible by 2"); 932 size_t count = chunkData.length / 2; 933 934 size_t ci; 935 foreach(i; hIST.length - count .. hIST.length) { 936 ubyte[2] values; 937 values[] = chunkData[ci .. ci + 2]; 938 *hIST ~= bigEndianToNative!ushort(values); 939 940 ci += 2; 941 } 942 } 943 944 void readChunk_tIME(ubyte[] chunkData) @trusted { 945 if (chunkData.length != 7) 946 throw allocator.make!ImageNotLoadableException("tIME chunk must be 7 bytes"); 947 948 int[6] values = [ 949 bigEndianToNative!short(cast(ubyte[2])chunkData[0 .. 2]), 950 chunkData[2], chunkData[3], chunkData[4], chunkData[5], chunkData[6] 951 ]; 952 953 if ((values[1] >= 1 && values[1] <= 12) && 954 (values[2] >= 1 && values[2] <= 31) && 955 (values[3] >= 0 && values[3] <= 23) && 956 (values[4] >= 0 && values[4] <= 59) && 957 (values[5] >= 0 && values[5] <= 60)) {} 958 else 959 throw allocator.make!ImageNotLoadableException("tIME chunk date time value is invalid"); 960 961 tIME = allocator.make!DateTime(values[0], values[1], values[2], values[3], values[4], values[5]); 962 } 963 964 static if (!is(Color == HeadersOnly)) { 965 void readChunk_IDAT(ubyte[] chunkData) @trusted { 966 import std.zlib : uncompress; // FIXME: std.zlib allocates without using the allocator *grumbles* 967 import std.math : ceil, floor; 968 969 // a simple check 970 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7 || IHDR.interlaceMethod == PngIHDRInterlaceMethod.NoInterlace) 971 {} else 972 throw allocator.make!ImageNotLoadableException("IDAT unknown interlace method"); 973 974 if (IHDR.compressionMethod == PngIHDRCompresion.DeflateInflate) {} 975 else 976 throw allocator.make!ImageNotLoadableException("IDAT unknown compression method"); 977 978 // constants 979 size_t pixelPreviousByteAmount; 980 size_t totalSize, scanLineSize; 981 size_t[7] /+rowsPerPass, +/scanLinesSize; 982 983 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 984 bool isGrayScale = (IHDR.colorType & PngIHDRColorType.Grayscale) == PngIHDRColorType.Grayscale; 985 bool isPalette = (IHDR.colorType & PngIHDRColorType.Palette) == PngIHDRColorType.Palette; 986 bool isColor = (IHDR.colorType & PngIHDRColorType.ColorUsed) == PngIHDRColorType.ColorUsed; 987 988 // some needed variables, in future processing 989 ubyte[] decompressed, previousScanLine, tempBitDepth124, myAdaptiveOffsets; 990 ubyte pass, sampleSize, pixelSampleSize; 991 size_t offsetX, offsetY, offset, currentRow; 992 993 final switch(IHDR.colorType) { 994 case PngIHDRColorType.AlphaChannelUsed: 995 sampleSize = 2; 996 break; 997 case PngIHDRColorType.PalletteWithColorUsed: 998 case PngIHDRColorType.Palette: 999 case PngIHDRColorType.Grayscale: 1000 sampleSize = 1; 1001 break; 1002 case PngIHDRColorType.ColorUsedWithAlpha: 1003 sampleSize = 4; 1004 break; 1005 case PngIHDRColorType.ColorUsed: 1006 sampleSize = 3; 1007 break; 1008 } 1009 1010 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1011 pixelSampleSize = cast(ubyte)(sampleSize + sampleSize); 1012 pixelPreviousByteAmount = pixelSampleSize; 1013 scanLineSize = pixelSampleSize * IHDR.width; 1014 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1015 pixelSampleSize = sampleSize; 1016 pixelPreviousByteAmount = sampleSize; 1017 scanLineSize = sampleSize * IHDR.width; 1018 } else { 1019 pixelSampleSize = sampleSize; 1020 pixelPreviousByteAmount = 1; 1021 tempBitDepth124 = alloc.makeArray!ubyte(8); 1022 1023 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth1) 1024 scanLineSize = cast(size_t)ceil((sampleSize * IHDR.width) / 8f); 1025 else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth2) 1026 scanLineSize = cast(size_t)ceil((sampleSize * IHDR.width) / 4f); 1027 else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth4) 1028 scanLineSize = cast(size_t)ceil((sampleSize * IHDR.width) / 2f); 1029 } 1030 1031 totalSize = IHDR.width * pixelSampleSize * IHDR.height; 1032 1033 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 1034 scanLineSize += 1; 1035 totalSize += IHDR.height; 1036 } 1037 1038 // no point calculating this if we are not gonna use it! 1039 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) { 1040 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) 1041 myAdaptiveOffsets = alloc.makeArray!ubyte(IHDR.height); 1042 1043 // calculates the length of each scan line per pass (Adam7) 1044 for(pass = 0; pass < 7; pass++) { 1045 if (starting_row[pass] >= IHDR.height) { 1046 scanLinesSize[pass] = 0; 1047 } else { 1048 float tscanLineSize = IHDR.width * pixelSampleSize; 1049 tscanLineSize -= starting_col[pass]; 1050 tscanLineSize /= col_increment[pass]; 1051 1052 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth1) 1053 tscanLineSize /= 8f; 1054 else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth2) 1055 tscanLineSize /= 4f; 1056 else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth4) 1057 tscanLineSize /= 2f; 1058 1059 tscanLineSize = ceil(tscanLineSize); 1060 1061 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) 1062 tscanLineSize += 1; 1063 1064 if (tscanLineSize <= 1) 1065 tscanLineSize = 0; 1066 1067 scanLinesSize[pass] = cast(size_t)tscanLineSize; 1068 } 1069 } 1070 } 1071 1072 // decompress 1073 decompressed = cast(ubyte[])uncompress(chunkData, totalSize); 1074 1075 void assignPixel(ColorP)(ColorP valuec) { 1076 // store color at coordinate 1077 static if (is(ColorP == Color)) 1078 value.setPixel(offsetX, offsetY, valuec); 1079 else 1080 value.setPixel(offsetX, offsetY, valuec.convertColor!Color); 1081 1082 // changes x and y coordinates for no interlace 1083 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.NoInterlace) { 1084 offsetX++; 1085 1086 if (offsetX == IHDR.width) { 1087 offsetX = 0; 1088 offsetY++; 1089 } 1090 } else if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) 1091 offsetX += col_increment[pass]; 1092 } 1093 1094 void grabPixelsFromScanLine(ubyte[] scanLine) { 1095 bool useMultiByte = IHDR.bitDepth == PngIHDRBitDepth.BitDepth16; 1096 1097 void handleSamples(ubyte[] samples) { 1098 while(samples.length > 0) { 1099 if (isPalette) { 1100 ubyte v = samples[0]; 1101 if (v >= PLTE.colors.length) 1102 throw allocator.make!ImageNotLoadableException("IDAT unknown palette color"); 1103 1104 assignPixel(PLTE.colors[v]); 1105 samples = samples[1 .. $]; 1106 } else if (useMultiByte) { 1107 if (isColor) { 1108 ushort[4] values; 1109 values[0] = bigEndianToNative!ushort(cast(ubyte[2])samples[0 .. 2]); 1110 values[1] = bigEndianToNative!ushort(cast(ubyte[2])samples[2 .. 4]); 1111 values[2] = bigEndianToNative!ushort(cast(ubyte[2])samples[4 .. 6]); 1112 1113 if (withAlpha) { 1114 values[3] = bigEndianToNative!ushort(cast(ubyte[2])samples[6 .. 8]); 1115 assignPixel(RGBA16(values[0], values[1], values[2], values[3])); 1116 samples = samples[8 .. $]; 1117 } else { 1118 assignPixel(RGB16(values[0], values[1], values[2])); 1119 samples = samples[6 .. $]; 1120 } 1121 } else if (isGrayScale) { 1122 ushort v = bigEndianToNative!ushort(cast(ubyte[2])samples[0 .. 2]); 1123 1124 if (withAlpha) { 1125 assignPixel(RGBA16(v, v, v, bigEndianToNative!ushort(cast(ubyte[2])samples[2 .. 4]))); 1126 samples = samples[4 .. $]; 1127 } else { 1128 assignPixel(RGB16(v, v, v)); 1129 samples = samples[2 .. $]; 1130 } 1131 } 1132 } else { 1133 if (isColor) { 1134 if (withAlpha) { 1135 assignPixel(RGBA8(samples[0], samples[1], samples[2], samples[3])); 1136 samples = samples[4 .. $]; 1137 } else { 1138 assignPixel(RGB8(samples[0], samples[1], samples[2])); 1139 samples = samples[3 .. $]; 1140 } 1141 } else if (isGrayScale) { 1142 ubyte v = samples[0]; 1143 1144 if (withAlpha) { 1145 assignPixel(RGBA8(v, v, v, samples[1])); 1146 samples = samples[2 .. $]; 1147 } else { 1148 assignPixel(RGB8(v, v, v)); 1149 samples = samples[1 .. $]; 1150 } 1151 } 1152 } 1153 } 1154 } 1155 1156 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16 || IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1157 handleSamples(scanLine); 1158 } else { 1159 ptrdiff_t maxSamples = IHDR.width; 1160 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) 1161 maxSamples = cast(size_t)ceil((maxSamples - starting_col[pass]) / cast(float)col_increment[pass]); 1162 maxSamples *= sampleSize; 1163 1164 // 1, 2, 4 bit depths 1165 foreach(scb; scanLine) { 1166 ubyte[] samples; 1167 1168 if (maxSamples <= 0) 1169 return; 1170 1171 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth4) { 1172 samples = tempBitDepth124[0 .. 2]; 1173 1174 samples[1] = cast(ubyte)((scb & 15) >> 0); 1175 samples[0] = cast(ubyte)((scb & 240) >> 4); 1176 1177 if (!isPalette) { 1178 samples[0] *= 17; 1179 samples[1] *= 17; 1180 } 1181 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth2) { 1182 samples = tempBitDepth124[0 .. 4]; 1183 1184 samples[3] = cast(ubyte)((scb & 3) >> 0); 1185 samples[2] = cast(ubyte)((scb & 12) >> 2); 1186 samples[1] = cast(ubyte)((scb & 48) >> 4); 1187 samples[0] = cast(ubyte)((scb & 192) >> 6); 1188 1189 if (!isPalette) { 1190 samples[0] *= 85; 1191 samples[1] *= 85; 1192 samples[2] *= 85; 1193 samples[3] *= 85; 1194 } 1195 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth1) { 1196 samples = tempBitDepth124[0 .. 8]; 1197 1198 samples[7] = cast(ubyte)((scb & 1) >> 0); 1199 samples[6] = cast(ubyte)((scb & 2) >> 1); 1200 samples[5] = cast(ubyte)((scb & 4) >> 2); 1201 samples[4] = cast(ubyte)((scb & 8) >> 3); 1202 samples[3] = cast(ubyte)((scb & 16) >> 4); 1203 samples[2] = cast(ubyte)((scb & 32) >> 5); 1204 samples[1] = cast(ubyte)((scb & 64) >> 6); 1205 samples[0] = cast(ubyte)((scb & 128) >> 7); 1206 1207 if (!isPalette) { 1208 samples[0] *= 255; 1209 samples[1] *= 255; 1210 samples[2] *= 255; 1211 samples[3] *= 255; 1212 samples[4] *= 255; 1213 samples[5] *= 255; 1214 samples[6] *= 255; 1215 samples[7] *= 255; 1216 } 1217 } 1218 1219 if (samples.length <= maxSamples) 1220 handleSamples(samples); 1221 else 1222 handleSamples(samples[0 .. maxSamples]); 1223 maxSamples -= samples.length; 1224 } 1225 } 1226 } 1227 1228 // defilters the scan line 1229 previousScanLine = null; 1230 void scanLineDefilter(ubyte adaptiveOffset, ubyte[] scanLine) { 1231 // defilter 1232 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 1233 if (scanLine.length <= 0) { 1234 previousScanLine = null; 1235 return; 1236 } 1237 1238 foreach(i; 0 .. scanLine.length) { 1239 switch(adaptiveOffset) { 1240 case 1: // sub 1241 // Sub(x) + Raw(x-bpp) 1242 1243 if (i >= pixelPreviousByteAmount) { 1244 ubyte rawSub = scanLine[i-pixelPreviousByteAmount]; 1245 scanLine[i] = cast(ubyte)(scanLine[i] + rawSub); 1246 } else { 1247 // no changes needed 1248 } 1249 1250 break; 1251 1252 case 2: // up 1253 // Up(x) + Prior(x) 1254 1255 if (previousScanLine.length > i) { 1256 ubyte prior = previousScanLine[i]; 1257 scanLine[i] = cast(ubyte)(scanLine[i] + prior); 1258 } else { 1259 // no changes needed 1260 } 1261 break; 1262 1263 case 3: // average 1264 // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) 1265 1266 if (previousScanLine.length > i) { 1267 if (i >= pixelPreviousByteAmount) { 1268 ubyte prior = previousScanLine[i]; 1269 ubyte rawSub = scanLine[i-pixelPreviousByteAmount]; 1270 scanLine[i] = cast(ubyte)(scanLine[i] + floor(cast(real)(rawSub + prior) / 2f)); 1271 } else { 1272 ubyte prior = previousScanLine[i]; 1273 ubyte rawSub = 0; 1274 scanLine[i] = cast(ubyte)(scanLine[i] + floor(cast(real)(rawSub + prior) / 2f)); 1275 } 1276 } else if (i >= pixelPreviousByteAmount) { 1277 ubyte prior = 0; 1278 ubyte rawSub = scanLine[i-pixelPreviousByteAmount]; 1279 scanLine[i] = cast(ubyte)(scanLine[i] + floor(cast(real)(rawSub + prior) / 2f)); 1280 } else { 1281 // no changes needed 1282 } 1283 break; 1284 1285 case 4: // paeth 1286 // Paeth(x) + PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) 1287 1288 if (previousScanLine.length > i) { 1289 if (i >= pixelPreviousByteAmount) { 1290 ubyte prior = previousScanLine[i]; 1291 ubyte rawSub = scanLine[i-pixelPreviousByteAmount]; 1292 ubyte priorRawSub = previousScanLine[i-pixelPreviousByteAmount]; 1293 scanLine[i] = cast(ubyte)(scanLine[i] + PaethPredictor(rawSub, prior, priorRawSub)); 1294 } else { 1295 ubyte prior = previousScanLine[i]; 1296 ubyte rawSub = 0; 1297 ubyte priorRawSub = 0; 1298 scanLine[i] = cast(ubyte)(scanLine[i] + PaethPredictor(rawSub, prior, priorRawSub)); 1299 } 1300 } else if (i >= pixelPreviousByteAmount) { 1301 ubyte prior = 0; 1302 ubyte rawSub = scanLine[i-pixelPreviousByteAmount]; 1303 ubyte priorRawSub = 0; 1304 scanLine[i] = cast(ubyte)(scanLine[i] + PaethPredictor(rawSub, prior, priorRawSub)); 1305 } else { 1306 // no changes needed 1307 } 1308 1309 break; 1310 1311 default: 1312 case 0: // none 1313 break; 1314 } 1315 } 1316 } 1317 1318 previousScanLine = scanLine; 1319 grabPixelsFromScanLine(scanLine); 1320 } 1321 1322 size_t lastYStart = size_t.max; 1323 1324 // performs the actual parsing of the scanlines 1325 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) { 1326 pass = 0; 1327 1328 while(pass < 7) { 1329 offsetY = starting_row[pass]; 1330 scanLineSize = scanLinesSize[pass]; 1331 1332 if (scanLineSize == 0) { 1333 pass++; 1334 continue; 1335 } 1336 1337 while(offsetY < IHDR.height) { 1338 offsetX = starting_col[pass]; 1339 bool thisScanLine = IHDR.bitDepth != PngIHDRBitDepth.BitDepth1 || !(offsetY == lastYStart && offsetX > 0); 1340 1341 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 1342 if (thisScanLine) { 1343 myAdaptiveOffsets[offsetY] = decompressed[offset]; 1344 scanLineDefilter(myAdaptiveOffsets[offsetY], decompressed[offset + 1 .. offset + scanLineSize]); 1345 offset += scanLineSize; 1346 } else { 1347 scanLineDefilter(myAdaptiveOffsets[offsetY], decompressed[offset .. offset + (scanLineSize - 1)]); 1348 offset += (scanLineSize - 1); 1349 } 1350 } else { 1351 assert(0); 1352 } 1353 1354 lastYStart = offsetY; 1355 offsetY += row_increment[pass]; 1356 } 1357 1358 pass++; 1359 } 1360 } else { 1361 while(offset < decompressed.length) { 1362 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 1363 scanLineDefilter(decompressed[offset], decompressed[offset + 1 .. offset + scanLineSize]); 1364 } else { 1365 assert(0); 1366 } 1367 offset += scanLineSize; 1368 } 1369 } 1370 1371 alloc.dispose(tempBitDepth124); 1372 } 1373 } 1374 1375 void readChunk_IEND(ubyte[] chunkData) @safe { 1376 // IEND chunk should be the last one. 1377 // It doesn't do anything special other then say, this is the end. 1378 // Now stop looking for more chunks! 1379 } 1380 1381 /* 1382 * The exporter 1383 */ 1384 1385 managed!(ubyte[]) performExport() @trusted { 1386 import std.digest.crc : crc32Of; 1387 ubyte[] buffer = allocator.makeArray!ubyte((1024 * 1024 * 8) + 4); // 8mb 1388 assert(buffer.length > 0); 1389 1390 import core.memory : GC; 1391 GC.disable; 1392 1393 ubyte[] ret = allocator.makeArray!ubyte(8); 1394 ret[0 .. 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; 1395 1396 void writeChunk(char[4] name, ubyte[] data) @trusted { 1397 size_t len = data.length + 12; // name + length + crc 1398 allocator.expandArray(ret, len); 1399 1400 ret[$-len .. $][0 .. 4] = nativeToBigEndian(cast(uint)data.length); 1401 ret[$-len .. $][4 .. 8] = cast(ubyte[4])name[]; 1402 ret[$-len .. $][8 .. $-4] = data[]; 1403 1404 buffer[0 .. 4] = cast(ubyte[4])name[]; 1405 ret[$-4 .. $] = nativeToBigEndian(*cast(uint*)crc32Of(buffer[0 .. data.length + 4]).ptr); 1406 } 1407 1408 writeChunk_IHDR(buffer[4 .. $], &writeChunk); 1409 if (gAMA !is null) 1410 writeChunk_gAMA(buffer[4 .. $], &writeChunk); 1411 if (PLTE !is null) 1412 writeChunk_PLTE(buffer[4 .. $], &writeChunk); 1413 if (tRNS !is null) 1414 writeChunk_tRNS(buffer[4 .. $], &writeChunk); 1415 if (cHRM !is null) 1416 writeChunk_cHRM(buffer[4 .. $], &writeChunk); 1417 if (sRGB !is null) 1418 writeChunk_sRGB(buffer[4 .. $], &writeChunk); 1419 if (iCCP !is null) 1420 writeChunk_iCCP(buffer[4 .. $], &writeChunk); 1421 1422 writeChunk_tEXt(buffer[4 .. $], &writeChunk); 1423 writeChunk_zEXt(buffer[4 .. $], &writeChunk); 1424 1425 if (bKGD !is null) 1426 writeChunk_bKGD(buffer[4 .. $], &writeChunk); 1427 if (pPHs !is null) 1428 writeChunk_pPHs(buffer[4 .. $], &writeChunk); 1429 if (sBIT !is null) 1430 writeChunk_sBIT(buffer[4 .. $], &writeChunk); 1431 if (sPLT.length > 0) 1432 writeChunk_sPLT(buffer[4 .. $], &writeChunk); 1433 if (hIST.length > 0) 1434 writeChunk_hIST(buffer[4 .. $], &writeChunk); 1435 if (tIME !is null) 1436 writeChunk_tIME(buffer[4 .. $], &writeChunk); 1437 1438 static if (!is(Color == HeadersOnly)) { 1439 writeChunk_IDAT(buffer[4 .. $], &writeChunk); 1440 } 1441 1442 // it contains nothing, so why bother having a dedicated method? 1443 writeChunk(cast(char[4])"IEND", null); 1444 1445 GC.enable; 1446 allocator.dispose(buffer); 1447 return managed!(ubyte[])(ret, managers(ReferenceCountedManager()), alloc); 1448 } 1449 1450 void writeChunk_IHDR(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1451 ubyte[] towrite; 1452 1453 towrite = buffer[0 .. 13]; 1454 towrite[0 .. 4] = nativeToBigEndian(IHDR.width); 1455 towrite[4 .. 8] = nativeToBigEndian(IHDR.height); 1456 towrite[8 .. 9] = nativeToBigEndian(IHDR.bitDepth); 1457 towrite[9 .. 10] = nativeToBigEndian(IHDR.colorType); 1458 towrite[10 .. 11] = nativeToBigEndian(IHDR.compressionMethod); 1459 towrite[11 .. 12] = nativeToBigEndian(IHDR.filterMethod); 1460 towrite[12 .. 13] = nativeToBigEndian(IHDR.interlaceMethod); 1461 1462 write(cast(char[4])"IHDR", towrite); 1463 } 1464 1465 void writeChunk_PLTE(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1466 ubyte[] towrite; 1467 1468 towrite = buffer[0 .. PLTE.colors.length * 3]; 1469 1470 size_t offset; 1471 foreach(i, c; PLTE.colors) { 1472 towrite[offset] = c.r.value; 1473 towrite[offset + 1] = c.g.value; 1474 towrite[offset + 2] = c.b.value; 1475 1476 offset += 3; 1477 } 1478 1479 write(cast(char[4])"PLTE", towrite); 1480 } 1481 1482 void writeChunk_tRNS(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1483 ubyte[] towrite; 1484 1485 if (IHDR.colorType & PngIHDRColorType.Palette) { 1486 towrite = buffer[0 .. tRNS.indexAlphas.length]; 1487 towrite[] = tRNS.indexAlphas[]; 1488 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 1489 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1490 towrite = buffer[0 .. 2]; 1491 towrite[] = nativeToBigEndian(tRNS.b16.r.value); 1492 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1493 towrite = buffer[0 .. 2]; 1494 towrite[0] = tRNS.b8.r.value; 1495 } else { 1496 towrite = buffer[0 .. 2]; 1497 towrite[0] = cast(ubyte)(tRNS.b8.r.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1498 } 1499 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 1500 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1501 towrite = buffer[0 .. 6]; 1502 towrite[0 .. 2] = nativeToBigEndian(tRNS.b16.r.value); 1503 towrite[2 .. 4] = nativeToBigEndian(tRNS.b16.g.value); 1504 towrite[4 .. 6] = nativeToBigEndian(tRNS.b16.b.value); 1505 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1506 towrite = buffer[0 .. 6]; 1507 towrite[0] = tRNS.b8.r.value; 1508 towrite[2] = tRNS.b8.g.value; 1509 towrite[4] = tRNS.b8.b.value; 1510 } else { 1511 towrite = buffer[0 .. 6]; 1512 towrite[0] = cast(ubyte)(tRNS.b8.r.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1513 towrite[2] = cast(ubyte)(tRNS.b8.g.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1514 towrite[4] = cast(ubyte)(tRNS.b8.b.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1515 } 1516 } 1517 1518 write(cast(char[4])"tRNS", towrite); 1519 } 1520 1521 void writeChunk_gAMA(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1522 ubyte[] towrite = buffer[0 .. 4]; 1523 towrite[] = nativeToBigEndian(gAMA.value); 1524 write(cast(char[4])"gAMA", towrite); 1525 } 1526 1527 void writeChunk_cHRM(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1528 ubyte[] towrite = buffer[0 .. 32]; 1529 towrite[0 .. 4] = nativeToBigEndian(cHRM.white_x); 1530 towrite[4 .. 8] = nativeToBigEndian(cHRM.white_y); 1531 towrite[8 .. 12] = nativeToBigEndian(cHRM.red_x); 1532 towrite[12 .. 16] = nativeToBigEndian(cHRM.red_y); 1533 towrite[16 .. 20] = nativeToBigEndian(cHRM.green_x); 1534 towrite[20 .. 24] = nativeToBigEndian(cHRM.green_y); 1535 towrite[24 .. 28] = nativeToBigEndian(cHRM.blue_x); 1536 towrite[28 .. 32] = nativeToBigEndian(cHRM.blue_y); 1537 write(cast(char[4])"cHRM", towrite); 1538 } 1539 1540 void writeChunk_sRGB(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1541 ubyte[] towrite = buffer[0 .. 1]; 1542 towrite[0] = sRGB.intent; 1543 write(cast(char[4])"sRGB", towrite); 1544 } 1545 1546 void writeChunk_iCCP(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1547 import std.zlib : compress; 1548 ubyte[] towrite; 1549 1550 towrite = buffer[0 .. iCCP.profileName.length + 2]; 1551 towrite[0 .. $-2] = cast(ubyte[])iCCP.profileName[]; 1552 towrite[$-2] = '\0'; 1553 towrite[$-1] = cast(ubyte)iCCP.compressionMethod; 1554 1555 ubyte[] compressed = cast(ubyte[])compress(iCCP.profile); 1556 towrite = buffer[0 .. towrite.length + compressed.length]; 1557 towrite[$-compressed.length .. $] = compressed[]; 1558 1559 write(cast(char[4])"iCCP", towrite); 1560 } 1561 1562 void writeChunk_tEXt(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1563 foreach(const(PngTextKeywords) keyword, string value; *tEXt) { 1564 ubyte[] towrite = buffer[0 .. keyword.length + 1]; 1565 1566 towrite[0 .. $-1] = cast(ubyte[])keyword[]; 1567 towrite[$-1] = '\0'; 1568 1569 towrite = buffer[0 .. towrite.length + value.length]; 1570 towrite[$-value.length .. $] = cast(ubyte[])value[]; 1571 1572 write(cast(char[4])"tEXt", towrite); 1573 } 1574 } 1575 1576 void writeChunk_zEXt(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1577 import std.zlib : compress; 1578 1579 foreach(const(PngTextKeywords) keyword, string value; *zEXt) { 1580 ubyte[] towrite = buffer[0 .. keyword.length + 2]; 1581 1582 towrite[0 .. $-2] = cast(ubyte[])keyword[]; 1583 towrite[$-2] = '\0'; 1584 towrite[$-1] = PngIHDRCompresion.DeflateInflate; 1585 1586 // FIXME: allocates 1587 ubyte[] compressed = cast(ubyte[])compress(value); 1588 1589 towrite = buffer[0 .. towrite.length + compressed.length]; 1590 towrite[$-compressed.length .. $] = compressed[]; 1591 1592 write(cast(char[4])"zEXt", towrite); 1593 } 1594 } 1595 1596 void writeChunk_bKGD(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1597 ubyte[] towrite; 1598 1599 if (IHDR.colorType & PngIHDRColorType.Palette) { 1600 towrite = buffer[0 .. 1]; 1601 towrite[0] = bKGD.index; 1602 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 1603 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1604 towrite = buffer[0 .. 2]; 1605 towrite[] = nativeToBigEndian(bKGD.b16.r.value); 1606 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1607 towrite = buffer[0 .. 2]; 1608 towrite[0] = bKGD.b8.r.value; 1609 } else { 1610 towrite = buffer[0 .. 2]; 1611 towrite[0] = cast(ubyte)(bKGD.b8.r.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1612 } 1613 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 1614 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1615 towrite = buffer[0 .. 6]; 1616 towrite[0 .. 2] = nativeToBigEndian(bKGD.b16.r.value); 1617 towrite[2 .. 4] = nativeToBigEndian(bKGD.b16.g.value); 1618 towrite[4 .. 6] = nativeToBigEndian(bKGD.b16.b.value); 1619 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1620 towrite = buffer[0 .. 6]; 1621 towrite[0] = bKGD.b8.r.value; 1622 towrite[2] = bKGD.b8.g.value; 1623 towrite[4] = bKGD.b8.b.value; 1624 } else { 1625 towrite = buffer[0 .. 6]; 1626 towrite[0] = cast(ubyte)(bKGD.b8.r.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1627 towrite[2] = cast(ubyte)(bKGD.b8.g.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1628 towrite[4] = cast(ubyte)(bKGD.b8.b.value / cast(ubyte)(256f/(2^(cast(ubyte)IHDR.bitDepth))-1)); 1629 } 1630 } 1631 1632 write(cast(char[4])"bKGD", towrite); 1633 } 1634 1635 void writeChunk_pPHs(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1636 ubyte[] towrite = buffer[0 .. 9]; 1637 1638 towrite[0 .. 4] = nativeToBigEndian(pPHs.ppx); 1639 towrite[4 .. 8] = nativeToBigEndian(pPHs.ppy); 1640 towrite[8] = pPHs.unit; 1641 1642 write(cast(char[4])"pPHs", towrite); 1643 } 1644 1645 void writeChunk_sBIT(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1646 ubyte[] towrite; 1647 1648 if (IHDR.colorType & PngIHDRColorType.Palette) { 1649 towrite = buffer[0 .. 3]; 1650 towrite[] = sBIT.indexed[]; 1651 } else if (IHDR.colorType & PngIHDRColorType.Grayscale) { 1652 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 1653 1654 if (withAlpha) { 1655 towrite = buffer[0 .. 2]; 1656 towrite[] = sBIT.grayScaleAlpha[]; 1657 } else { 1658 towrite = buffer[0 .. 1]; 1659 towrite[0] = sBIT.grayScale; 1660 } 1661 } else if (IHDR.colorType & PngIHDRColorType.ColorUsed) { 1662 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 1663 1664 if (withAlpha) { 1665 towrite = buffer[0 .. 4]; 1666 towrite[] = sBIT.trueColorAlpha[]; 1667 } else { 1668 towrite = buffer[0 .. 3]; 1669 towrite[] = sBIT.trueColor[]; 1670 } 1671 } 1672 1673 write(cast(char[4])"sBIT", towrite); 1674 } 1675 1676 void writeChunk_sPLT(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1677 ubyte[] towrite; 1678 1679 foreach(chunk; *sPLT) { 1680 towrite = buffer[towrite.length .. towrite.length + chunk.paletteName.length + 2]; 1681 towrite[$-(chunk.paletteName.length + 2) .. $-2] = cast(ubyte[])chunk.paletteName[]; 1682 towrite[$-2] = '\0'; 1683 towrite[$-1] = chunk.sampleDepth; 1684 1685 size_t count; 1686 if (chunk.sampleDepth == PngIHDRBitDepth.BitDepth8) { 1687 size_t offset = towrite.length; 1688 towrite = buffer[0 .. towrite.length + (chunk.colors.length * 6)]; 1689 1690 foreach(c; chunk.colors) { 1691 towrite[offset] = c.color.b8.r.value; 1692 towrite[offset + 1] = c.color.b8.g.value; 1693 towrite[offset + 2] = c.color.b8.b.value; 1694 towrite[offset + 3] = c.color.b8.a.value; 1695 towrite[offset + 4 .. offset + 6] = nativeToBigEndian(c.frequency); 1696 1697 offset += 6; 1698 } 1699 } else if (chunk.sampleDepth == PngIHDRBitDepth.BitDepth16) { 1700 size_t offset = towrite.length; 1701 towrite = buffer[0 .. towrite.length + (chunk.colors.length * 10)]; 1702 1703 foreach(c; chunk.colors) { 1704 towrite[offset .. offset + 2] = nativeToBigEndian(c.color.b16.r.value); 1705 towrite[offset + 2 .. offset + 4] = nativeToBigEndian(c.color.b16.g.value); 1706 towrite[offset + 4 .. offset + 6] = nativeToBigEndian(c.color.b16.b.value); 1707 towrite[offset + 6 .. offset + 8] = nativeToBigEndian(c.color.b16.a.value); 1708 towrite[offset + 8 .. offset + 10] = nativeToBigEndian(c.frequency); 1709 1710 offset += 10; 1711 } 1712 } else { 1713 // TODO: ugh oh, this is not good! 1714 } 1715 } 1716 1717 write(cast(char[4])"sPLT", towrite); 1718 } 1719 1720 void writeChunk_hIST(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1721 ubyte[] towrite = buffer[0 .. hIST.length * 2]; 1722 1723 size_t offset; 1724 foreach(v; *hIST) { 1725 towrite[offset .. offset + 2] = nativeToBigEndian(v); 1726 1727 offset += 2; 1728 } 1729 1730 write(cast(char[4])"hIST", towrite); 1731 } 1732 1733 void writeChunk_tIME(ubyte[] buffer, void delegate(char[4], ubyte[]) write) @trusted { 1734 ubyte[] towrite = buffer[0 .. 7]; 1735 1736 towrite[0 .. 2] = nativeToBigEndian(tIME.year); 1737 towrite[2] = tIME.month; 1738 towrite[3] = tIME.day; 1739 towrite[4] = tIME.hour; 1740 towrite[5] = tIME.minute; 1741 towrite[6] = tIME.second; 1742 1743 write(cast(char[4])"tIME", towrite); 1744 } 1745 1746 static if (!is(Color == HeadersOnly)) { 1747 void writeChunk_IDAT(ubyte[] buffer, void delegate(char[4], ubyte[]) theWriteFunc) @trusted { 1748 import std.zlib : compress; 1749 import std.math : ceil, floor; 1750 1751 ubyte findPLTEColor(Color c1) { 1752 RGB8 c = convertColor!RGB8(c1); 1753 1754 foreach(i, c2; PLTE.colors) { 1755 if (i >= 256) 1756 break; 1757 1758 if (c2 == c) { 1759 return cast(ubyte)i; 1760 } 1761 } 1762 1763 throw alloc.make!ImageNotExportableException("Palette not completed with all colors."); 1764 } 1765 1766 // a simple check 1767 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7 || IHDR.interlaceMethod == PngIHDRInterlaceMethod.NoInterlace) 1768 {} else 1769 throw allocator.make!ImageNotLoadableException("IDAT unknown interlace method"); 1770 1771 if (IHDR.compressionMethod == PngIHDRCompresion.DeflateInflate) {} 1772 else 1773 throw allocator.make!ImageNotLoadableException("IDAT unknown compression method"); 1774 1775 // constants 1776 size_t pixelPreviousByteAmount, rowSize; 1777 ubyte sampleSize, pixelSampleSize, scanLineFilterMethodOffset; 1778 1779 bool withAlpha = (IHDR.colorType & PngIHDRColorType.AlphaChannelUsed) == PngIHDRColorType.AlphaChannelUsed; 1780 bool isGrayScale = (IHDR.colorType & PngIHDRColorType.Grayscale) == PngIHDRColorType.Grayscale; 1781 bool isPalette = (IHDR.colorType & PngIHDRColorType.Palette) == PngIHDRColorType.Palette; 1782 bool isColor = (IHDR.colorType & PngIHDRColorType.ColorUsed) == PngIHDRColorType.ColorUsed; 1783 1784 // some needed variables, in future processing 1785 ubyte[] decompressed, previousScanLine, tempFilterPrevious, tempFilterCurrent, tempScanLine, currentScanLine; 1786 ubyte pass, byteToOffset; 1787 size_t offsetX, offsetY; 1788 1789 final switch(IHDR.colorType) { 1790 case PngIHDRColorType.AlphaChannelUsed: 1791 sampleSize = 2; 1792 break; 1793 case PngIHDRColorType.PalletteWithColorUsed: 1794 case PngIHDRColorType.Palette: 1795 case PngIHDRColorType.Grayscale: 1796 sampleSize = 1; 1797 break; 1798 case PngIHDRColorType.ColorUsedWithAlpha: 1799 sampleSize = 4; 1800 break; 1801 case PngIHDRColorType.ColorUsed: 1802 sampleSize = 3; 1803 break; 1804 } 1805 1806 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1807 pixelSampleSize = cast(ubyte)(sampleSize + sampleSize); 1808 pixelPreviousByteAmount = pixelSampleSize; 1809 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1810 pixelSampleSize = sampleSize; 1811 pixelPreviousByteAmount = sampleSize; 1812 } else { 1813 pixelSampleSize = sampleSize; 1814 pixelPreviousByteAmount = 1; 1815 } 1816 1817 rowSize = IHDR.width * pixelSampleSize; 1818 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) 1819 rowSize++; 1820 1821 tempFilterPrevious = alloc.makeArray!ubyte(rowSize); 1822 tempFilterCurrent = alloc.makeArray!ubyte(rowSize); 1823 tempScanLine = alloc.makeArray!ubyte(rowSize); 1824 1825 // filters the scan line 1826 previousScanLine = null; 1827 void filterScanLine(ubyte[] scanLine) { 1828 // filter 1829 tempFilterCurrent[0 .. scanLine.length] = scanLine[]; 1830 1831 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 1832 if (scanLine.length <= 1) { 1833 previousScanLine = tempFilterPrevious[0 .. 0]; 1834 return; 1835 } 1836 ubyte adaptiveOffset = scanLine[0]; 1837 1838 foreach(i; 1 .. scanLine.length) { 1839 switch(adaptiveOffset) { 1840 case 1: // sub 1841 // Sub(x) - Raw(x-bpp) 1842 1843 if (i-1 >= pixelPreviousByteAmount) { 1844 ubyte rawSub = tempFilterCurrent[i-pixelPreviousByteAmount]; 1845 scanLine[i] = cast(ubyte)(scanLine[i] - rawSub); 1846 } else { 1847 // no changes needed 1848 } 1849 break; 1850 1851 case 2: // up 1852 // Up(x) - Prior(x) 1853 1854 if (previousScanLine.length > i) { 1855 ubyte prior = previousScanLine[i]; 1856 scanLine[i] = cast(ubyte)(scanLine[i] - prior); 1857 } else { 1858 // no changes needed 1859 } 1860 break; 1861 1862 case 3: // average 1863 // Average(x) - floor((Raw(x-bpp)+Prior(x))/2) 1864 1865 if (previousScanLine.length > i) { 1866 if (i-1 >= pixelPreviousByteAmount) { 1867 ubyte prior = previousScanLine[i]; 1868 ubyte rawSub = tempFilterCurrent[i-pixelPreviousByteAmount]; 1869 scanLine[i] = cast(ubyte)(scanLine[i] - floor(cast(real)(rawSub + prior) / 2f)); 1870 } else { 1871 ubyte prior = previousScanLine[i]; 1872 ubyte rawSub = 0; 1873 scanLine[i] = cast(ubyte)(scanLine[i] - floor(cast(real)(rawSub + prior) / 2f)); 1874 } 1875 } else if (i-1 >= pixelPreviousByteAmount) { 1876 ubyte prior = 0; 1877 ubyte rawSub = tempFilterCurrent[i-pixelPreviousByteAmount]; 1878 scanLine[i] = cast(ubyte)(scanLine[i] - floor(cast(real)(rawSub + prior) / 2f)); 1879 } else { 1880 // no changes needed 1881 } 1882 break; 1883 1884 case 4: // paeth 1885 // Paeth(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) 1886 1887 if (previousScanLine.length > i) { 1888 if (i-1 >= pixelPreviousByteAmount) { 1889 ubyte prior = previousScanLine[i]; 1890 ubyte rawSub = tempFilterCurrent[i-pixelPreviousByteAmount]; 1891 ubyte priorRawSub = previousScanLine[i-pixelPreviousByteAmount]; 1892 scanLine[i] = cast(ubyte)(scanLine[i] - PaethPredictor(rawSub, prior, priorRawSub)); 1893 } else { 1894 ubyte prior = previousScanLine[i]; 1895 ubyte rawSub = 0; 1896 ubyte priorRawSub = 0; 1897 scanLine[i] = cast(ubyte)(scanLine[i] - PaethPredictor(rawSub, prior, priorRawSub)); 1898 } 1899 } else if (i-1 >= pixelPreviousByteAmount) { 1900 ubyte prior = 0; 1901 ubyte rawSub = tempFilterCurrent[i-pixelPreviousByteAmount]; 1902 ubyte priorRawSub = 0; 1903 scanLine[i] = cast(ubyte)(scanLine[i] - PaethPredictor(rawSub, prior, priorRawSub)); 1904 } else { 1905 // no changes needed 1906 } 1907 break; 1908 1909 default: 1910 case 0: // none 1911 break; 1912 } 1913 } 1914 } 1915 1916 if (decompressed is null) { 1917 decompressed = alloc.makeArray!ubyte(scanLine.length-scanLineFilterMethodOffset); 1918 } else { 1919 alloc.expandArray(decompressed, scanLine.length-scanLineFilterMethodOffset); 1920 } 1921 decompressed[$-(scanLine.length - scanLineFilterMethodOffset) .. $] = scanLine[scanLineFilterMethodOffset .. $]; 1922 1923 previousScanLine = tempFilterPrevious[0 .. scanLine.length]; 1924 previousScanLine[] = tempFilterCurrent[0 .. scanLine.length][]; 1925 } 1926 1927 const ubyte bitByteCount = cast(ubyte)(8 / IHDR.bitDepth); 1928 1929 void storeChannel(ubyte v) { 1930 if (byteToOffset == bitByteCount) 1931 byteToOffset = 0; 1932 if (byteToOffset == 0) 1933 currentScanLine = tempScanLine[0 .. currentScanLine.length + 1]; 1934 1935 if (byteToOffset > 0) { 1936 v = cast(ubyte)(v << ((8-IHDR.bitDepth)-(IHDR.bitDepth * byteToOffset))); 1937 currentScanLine[$-1] |= v; 1938 } else 1939 currentScanLine[$-1] = cast(ubyte)(v << (8-IHDR.bitDepth)); 1940 1941 byteToOffset++; 1942 } 1943 1944 void serializeScanLine(Color c) { 1945 // serailize the color as apropriete form into the scanLineBuffer 1946 1947 if (isPalette) { 1948 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8 || IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1949 currentScanLine = tempScanLine[0 .. currentScanLine.length + 1]; 1950 currentScanLine[$-1] = findPLTEColor(c); 1951 } else { 1952 storeChannel(findPLTEColor(c)); 1953 } 1954 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8 || IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1955 currentScanLine = tempScanLine[0 .. currentScanLine.length + pixelSampleSize]; 1956 1957 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth16) { 1958 RGBA16 pixToUse = convertColor!RGBA16(c); 1959 1960 if (isColor) { 1961 if (withAlpha) { 1962 currentScanLine[$-8 .. $-6] = nativeToBigEndian(pixToUse.r.value); 1963 currentScanLine[$-6 .. $-4] = nativeToBigEndian(pixToUse.g.value); 1964 currentScanLine[$-4 .. $-2] = nativeToBigEndian(pixToUse.b.value); 1965 currentScanLine[$-2 .. $] = nativeToBigEndian(pixToUse.a.value); 1966 } else { 1967 currentScanLine[$-6 .. $-4] = nativeToBigEndian(pixToUse.r.value); 1968 currentScanLine[$-4 .. $-2] = nativeToBigEndian(pixToUse.g.value); 1969 currentScanLine[$-2 .. $] = nativeToBigEndian(pixToUse.b.value); 1970 } 1971 } else if (isGrayScale) { 1972 float pixG = (pixToUse.r.value / 3f) + (pixToUse.g.value / 3f) + (pixToUse.b.value / 3f); 1973 1974 if (withAlpha) { 1975 currentScanLine[$-4 .. $-2] = nativeToBigEndian(cast(ushort)pixG); 1976 currentScanLine[$-2 .. $] = nativeToBigEndian(pixToUse.a.value); 1977 } else { 1978 currentScanLine[$-2 .. $] = nativeToBigEndian(cast(ushort)pixG); 1979 } 1980 } 1981 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth8) { 1982 RGBA8 pixToUse = convertColor!RGBA8(c); 1983 1984 if (isColor) { 1985 if (withAlpha) { 1986 currentScanLine[$-4 .. $-3] = pixToUse.r.value; 1987 currentScanLine[$-3 .. $-2] = pixToUse.g.value; 1988 currentScanLine[$-2 .. $-1] = pixToUse.b.value; 1989 currentScanLine[$-1 .. $] = pixToUse.a.value; 1990 } else { 1991 currentScanLine[$-3 .. $-2] = pixToUse.r.value; 1992 currentScanLine[$-2 .. $-1] = pixToUse.g.value; 1993 currentScanLine[$-1 .. $] = pixToUse.b.value; 1994 } 1995 } else if (isGrayScale) { 1996 float pixG = (pixToUse.r.value / 3f) + (pixToUse.g.value / 3f) + (pixToUse.b.value / 3f); 1997 1998 if (withAlpha) { 1999 currentScanLine[$-2 .. $-1] = cast(ubyte)pixG; 2000 currentScanLine[$-1 .. $] = pixToUse.a.value; 2001 } else { 2002 currentScanLine[$-1 .. $] = cast(ubyte)pixG; 2003 } 2004 } 2005 } 2006 } else { 2007 // 1, 2, 4 bit depths 2008 ubyte bitMaxValue; 2009 ubyte samplesPerPixel; 2010 2011 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth4) { 2012 bitMaxValue = 15; 2013 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth2) { 2014 bitMaxValue = 3; 2015 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth1) { 2016 bitMaxValue = 1; 2017 } 2018 2019 if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth4) { 2020 samplesPerPixel = 2; 2021 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth2) { 2022 samplesPerPixel = 4; 2023 } else if (IHDR.bitDepth == PngIHDRBitDepth.BitDepth1) { 2024 samplesPerPixel = 8; 2025 } else 2026 samplesPerPixel = 1; 2027 2028 RGBA8 pixToUse = convertColor!RGBA8(c); 2029 ubyte[4] bitDepthValues = [ 2030 cast(ubyte)ceil((pixToUse.r.value / 256f) * bitMaxValue), 2031 cast(ubyte)ceil((pixToUse.g.value / 256f) * bitMaxValue), 2032 cast(ubyte)ceil((pixToUse.b.value / 256f) * bitMaxValue), 2033 cast(ubyte)ceil((pixToUse.a.value / 256f) * bitMaxValue) 2034 ]; 2035 2036 if (isColor) { 2037 storeChannel(bitDepthValues[0]); 2038 storeChannel(bitDepthValues[1]); 2039 storeChannel(bitDepthValues[2]); 2040 2041 if (withAlpha) 2042 storeChannel(bitDepthValues[3]); 2043 } else if (isGrayScale) { 2044 storeChannel(bitDepthValues[0]); 2045 2046 if (withAlpha) 2047 storeChannel(bitDepthValues[3]); 2048 } 2049 } 2050 } 2051 2052 void startScanLine() { 2053 if (IHDR.filterMethod == PngIHDRFilter.Adaptive) { 2054 const ubyte[] filtersToApply = [cast(ubyte)0, 1]; 2055 currentScanLine = tempScanLine[0 .. 1]; 2056 2057 //if (isPalette) 2058 currentScanLine[0] = 0; 2059 //else 2060 // currentScanLine[0] = filtersToApply[offsetY % filtersToApply.length]; 2061 2062 scanLineFilterMethodOffset = 0; 2063 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) { 2064 if (offsetX > 0) { 2065 //scanLineFilterMethodOffset = 1; 2066 } 2067 } 2068 } else 2069 currentScanLine = tempScanLine[0 .. 0]; 2070 byteToOffset = 0; // 1, 2, 4 bit depth 2071 } 2072 2073 bool[size_t][size_t] doneSets; 2074 2075 if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.Adam7) { 2076 pass = 0; 2077 while(pass < 7) { 2078 offsetY = starting_row[pass]; 2079 2080 while(offsetY < IHDR.height) { 2081 offsetX = starting_col[pass]; 2082 startScanLine(); 2083 2084 while(offsetX < IHDR.width) { 2085 assert(doneSets.get(offsetY, null).get(offsetX, true)); 2086 doneSets[offsetY][offsetX] = true; 2087 serializeScanLine(value[offsetX, offsetY]); 2088 offsetX += col_increment[pass]; 2089 } 2090 2091 filterScanLine(currentScanLine); 2092 offsetY += row_increment[pass]; 2093 } 2094 2095 pass++; 2096 } 2097 } else if (IHDR.interlaceMethod == PngIHDRInterlaceMethod.NoInterlace) { 2098 foreach(y; 0 .. IHDR.height) { 2099 offsetY = y; 2100 offsetX = 0; 2101 startScanLine(); 2102 2103 foreach(x; 0 .. IHDR.width) { 2104 offsetX = x; 2105 2106 assert(doneSets.get(offsetY, null).get(offsetX, true)); 2107 doneSets[offsetY][offsetX] = true; 2108 serializeScanLine(value[offsetX, offsetY]); 2109 } 2110 2111 filterScanLine(currentScanLine); 2112 } 2113 } 2114 2115 foreach(x; 0 .. IHDR.width) { 2116 foreach(y; 0 .. IHDR.height) { 2117 assert(doneSets.get(y, null).get(x, false)); 2118 } 2119 } 2120 2121 ubyte[] compressed; 2122 2123 // compress 2124 if (IHDR.compressionMethod == PngIHDRCompresion.DeflateInflate) { 2125 compressed = cast(ubyte[])compress(decompressed); 2126 } else { 2127 throw allocator.make!ImageNotLoadableException("IDAT unknown compression method"); 2128 } 2129 2130 ubyte[] bfr2 = buffer[0 .. compressed.length]; 2131 bfr2[] = compressed[]; 2132 2133 theWriteFunc(cast(char[4])"IDAT", bfr2); 2134 2135 alloc.dispose(tempFilterPrevious); 2136 alloc.dispose(tempFilterCurrent); 2137 alloc.dispose(decompressed); 2138 } 2139 } 2140 2141 /* 2142 * Misc functions 2143 */ 2144 2145 void performCompatConfigure() { 2146 static if (!is(Color == HeadersOnly)) { 2147 IHDR.width = cast(uint)value.width; 2148 IHDR.height = cast(uint)value.height; 2149 2150 // TODO: better color space guessing 2151 2152 IHDR.bitDepth = PngIHDRBitDepth.BitDepth8; 2153 IHDR.colorType = PngIHDRColorType.ColorUsed; 2154 IHDR.compressionMethod = PngIHDRCompresion.DeflateInflate; 2155 IHDR.filterMethod = PngIHDRFilter.Adaptive; 2156 } 2157 } 2158 } 2159 } 2160 2161 /** 2162 * Loads a PNG file headers 2163 * 2164 * Can be used to determine which color type to use at runtime. 2165 * 2166 * Returns: 2167 * A PNG files headers without the image data 2168 */ 2169 managed!(PNGFileFormat!HeadersOnly) loadPNGHeaders(IR)(IR input, IAllocator allocator = theAllocator()) @trusted if (isInputRange!IR && is(ElementType!IR == ubyte)) { 2170 managed!(PNGFileFormat!HeadersOnly) ret = managed!(PNGFileFormat!HeadersOnly)(managers(ReferenceCountedManager()), tuple(allocator), allocator); 2171 ret.performInput(input); 2172 2173 return ret; 2174 } 2175 2176 /** 2177 * Loads a PNG file using specific color type 2178 * 2179 * Params: 2180 * input = Input range that returns the files bytes 2181 * allocator = The allocator to use the allocate the image 2182 * 2183 * Returns: 2184 * A PNG file, loaded as an image along with its headers. Using specified image storage type. 2185 */ 2186 managed!(PNGFileFormat!Color) loadPNG(Color, ImageImpl=ImageStorageHorizontal!Color, IR)(IR input, IAllocator allocator = theAllocator()) @trusted if (isInputRange!IR && is(ElementType!IR == ubyte) && isImage!ImageImpl) { 2187 managed!(PNGFileFormat!Color) ret = managed!(PNGFileFormat!Color)(managers(), tuple(allocator), allocator); 2188 2189 ret.theImageAllocator = &ret.allocateTheImage!ImageImpl; 2190 ret.performInput(input); 2191 2192 return ret; 2193 } 2194 2195 /// 2196 unittest { 2197 import std.experimental.color; 2198 import std.conv : hexString; 2199 2200 import std.file : read; 2201 auto input = cast(ubyte[])"89504E470D0A1A0A0000000D4948445200000002000000020802000000FDD49A73000000097048597300000B1300000B1301009A9C180000000774494D4507DF07150E122E54E72FF10000001974455874436F6D6D656E74004372656174656420776974682047494D5057810E17000000114944415408D763F8CFC0C0F09F014A000019F402FEE23BE1150000000049454E44AE426082".hexString; 2202 2203 managed!(PNGFileFormat!RGB8) image = loadPNG!RGB8(input); 2204 } 2205 2206 /** 2207 * Constructs a compatible version of an image as PNG 2208 * 2209 * Params: 2210 * form = The image to construct from 2211 * 2212 * Returns: 2213 * A compatible PNG image 2214 */ 2215 managed!(PNGFileFormat!Color) asPNG(From, Color = ImageColor!From, ImageImpl=ImageStorageHorizontal!Color)(From from, IAllocator allocator = theAllocator()) 2216 if (isImage!From && !is(From == struct)) { 2217 import devisualization.image.primitives : copyTo; 2218 2219 managed!(PNGFileFormat!Color) ret = managed!(PNGFileFormat!Color)(managers(), tuple(allocator), allocator); 2220 ret.allocateTheImage!ImageImpl(from.width, from.height); 2221 2222 from.copyTo(ret.value); 2223 ret.performCompatConfigure(); 2224 2225 return ret; 2226 } 2227 2228 managed!(PNGFileFormat!Color) asPNG(From, Color = ImageColor!From, ImageImpl=ImageStorageHorizontal!Color)(ref From from, IAllocator allocator = theAllocator()) @safe 2229 if (isImage!From && is(From == struct)) { 2230 return asPNG!(From, Color, ImageImpl)(&from, allocator); 2231 } 2232 2233 /// 2234 unittest { 2235 import std.experimental.color; 2236 import std.conv : hexString; 2237 2238 import std.file : read; 2239 auto input = cast(ubyte[])"89504E470D0A1A0A0000000D4948445200000002000000020802000000FDD49A73000000097048597300000B1300000B1301009A9C180000000774494D4507DF07150E122E54E72FF10000001974455874436F6D6D656E74004372656174656420776974682047494D5057810E17000000114944415408D763F8CFC0C0F09F014A000019F402FEE23BE1150000000049454E44AE426082".hexString; 2240 2241 ImageStorageHorizontal!RGB8 image = ImageStorageHorizontal!RGB8(2, 2); 2242 managed!(PNGFileFormat!RGB8) image2 = asPNG(&image); 2243 2244 // modify some fields 2245 2246 ubyte[] or = image2.toBytes(); 2247 } 2248 2249 /// 2250 enum PngTextKeywords : string { 2251 /// 2252 Title = "Title", 2253 /// 2254 Author = "Author", 2255 /// 2256 Description = "Description", 2257 /// 2258 Copyright = "Copyright", 2259 /// 2260 CreationTime = "Creation Time", 2261 /// 2262 Software = "Software", 2263 /// 2264 Disclaimer = "Disclaimer", 2265 /// 2266 Warning = "Warning", 2267 /// 2268 Source = "Source", 2269 /// 2270 Comment = "Comment" 2271 } 2272 2273 /// 2274 enum PngIHDRColorType : ubyte { 2275 /// 2276 Grayscale = 0, // valid (0) 2277 Palette = 1 << 0, // not valid 2278 /// 2279 ColorUsed = 1 << 1, // valid (2) rgb 2280 /// 2281 AlphaChannelUsed = 1 << 2, // valid (4) a 2282 /// 2283 PalletteWithColorUsed = Palette | ColorUsed, // valid (1, 2) index + alpha 2284 /// 2285 ColorUsedWithAlpha = ColorUsed | AlphaChannelUsed, // valid (2, 4) rgba 2286 /// 2287 GrayscaleWithAlpha = Grayscale | AlphaChannelUsed 2288 } 2289 2290 /// 2291 enum PngIHDRBitDepth : ubyte { 2292 // valid with color type: 2293 /// 2294 BitDepth1 = 1, // 0, 3 2295 /// 2296 BitDepth2 = 2, // 0, 3 2297 /// 2298 BitDepth4 = 4, // 0, 3 2299 /// 2300 BitDepth8 = 8, // 0, 2, 3, 4, 8 2301 /// 2302 BitDepth16 = 16 // 0, 2, 4, 8 2303 } 2304 2305 /// 2306 enum PngIHDRCompresion : ubyte { 2307 /// 2308 DeflateInflate = 0 2309 } 2310 2311 /// 2312 enum PngIHDRFilter : ubyte { 2313 /// 2314 Adaptive = 0 2315 } 2316 2317 /// 2318 enum PngIHDRInterlaceMethod : ubyte { 2319 /// 2320 NoInterlace = 0, 2321 /// 2322 Adam7 = 1 2323 } 2324 2325 /// 2326 enum PngRenderingIntent : ubyte { 2327 /// 2328 Perceptual = 0, 2329 /// 2330 RelativeColorimetric = 1, 2331 /// 2332 Saturation = 2, 2333 /// 2334 AbsoluteColorimetric = 3, 2335 /// 2336 Unknown=255 2337 } 2338 2339 /// 2340 enum PngPhysicalPixelUnit : ubyte { 2341 /// 2342 Unknown = 0, 2343 /// 2344 Meter = 1 2345 } 2346 2347 /// 2348 struct IHDR_Chunk { 2349 /// 2350 uint width; 2351 /// 2352 uint height; 2353 /// 2354 PngIHDRBitDepth bitDepth; 2355 /// 2356 PngIHDRColorType colorType; 2357 /// 2358 PngIHDRCompresion compressionMethod; 2359 /// 2360 PngIHDRFilter filterMethod; 2361 /// 2362 PngIHDRInterlaceMethod interlaceMethod; 2363 } 2364 2365 /// 2366 struct PLTE_Chunk { 2367 /// 2368 alias Color = RGB8; 2369 2370 /// 2371 Color[] colors; 2372 } 2373 2374 /// 2375 struct cHRM_Chunk { 2376 /// 2377 uint white_x; 2378 /// 2379 uint white_y; 2380 2381 /// 2382 uint red_x; 2383 /// 2384 uint red_y; 2385 2386 /// 2387 uint green_x; 2388 /// 2389 uint green_y; 2390 2391 /// 2392 uint blue_x; 2393 /// 2394 uint blue_y; 2395 } 2396 2397 /// 2398 struct sRGB_Chunk { 2399 /// 2400 PngRenderingIntent intent; 2401 } 2402 2403 /// 2404 struct iCCP_Chunk { 2405 /// 2406 string profileName; 2407 2408 /// 2409 PngIHDRCompresion compressionMethod; 2410 2411 /// 2412 ubyte[] profile; 2413 } 2414 2415 /// 2416 struct gAMA_Chunk { 2417 /// 2418 uint value; 2419 2420 /// 2421 @property void set(float value) { 2422 this.value = cast(uint)(value * 100000); 2423 } 2424 2425 /// 2426 @property float get() { 2427 return value / 100000f; 2428 } 2429 } 2430 2431 /// 2432 union tRNS_Chunk { 2433 /// 2434 ubyte[] indexAlphas; 2435 /// 2436 RGB8 b8; 2437 /// 2438 RGB16 b16; 2439 } 2440 2441 /// 2442 union bKGD_Chunk { 2443 /// 2444 ubyte index; 2445 /// 2446 RGB8 b8; 2447 /// 2448 RGB16 b16; 2449 } 2450 2451 /// 2452 struct pPHs_Chunk { 2453 /// 2454 uint ppx; 2455 2456 /// 2457 uint ppy; 2458 2459 /// 2460 PngPhysicalPixelUnit unit; 2461 } 2462 2463 /// 2464 union sBIT_Chunk { 2465 /// 2466 ubyte grayScale; 2467 /// 2468 ubyte[3] trueColor; 2469 /// 2470 ubyte[3] indexed; 2471 /// 2472 ubyte[2] grayScaleAlpha; 2473 /// 2474 ubyte[4] trueColorAlpha; 2475 } 2476 2477 /// 2478 struct sPLT_Chunk { 2479 /// 2480 string paletteName; 2481 2482 /// 2483 PngIHDRBitDepth sampleDepth; 2484 2485 /// 2486 struct Entry { 2487 /// 2488 union Color { 2489 /// 2490 RGBA8 b8; 2491 /// 2492 RGBA16 b16; 2493 } 2494 2495 /// 2496 Color color; 2497 2498 /// 2499 ushort frequency; 2500 } 2501 2502 /// 2503 Entry[] colors; 2504 } 2505 2506 /// Grumbles @ManuEvans... 2507 alias RGBA16 = RGB!("rgba", ushort); 2508 /// Grumbles @ManuEvans... 2509 alias RGB16 = RGB!("rgb", ushort); 2510 2511 private { 2512 struct IDAT_Chunk(Color) { 2513 ubyte[] data; 2514 } 2515 2516 enum { 2517 starting_row = [ 2518 0, 0, 4, 0, 2, 0, 1 2519 ], 2520 2521 starting_col = [ 2522 0, 4, 0, 2, 0, 1, 0 2523 ], 2524 2525 row_increment = [ 2526 8, 8, 8, 4, 4, 2, 2 2527 ], 2528 2529 col_increment = [ 2530 8, 8, 4, 4, 2, 2, 1 2531 ], 2532 2533 block_height = [ 2534 8, 8, 4, 4, 2, 2, 1 2535 ], 2536 2537 block_width = [ 2538 8, 4, 4, 2, 2, 1, 1 2539 ] 2540 } 2541 2542 ubyte PaethPredictor(ubyte a, ubyte b, ubyte c) { 2543 import std.math : abs; 2544 2545 // a = left, b = above, c = upper left 2546 int p = a + b - c; // initial estimate 2547 int pa = abs(p - a); // distances to a, b, c 2548 int pb = abs(p - b); 2549 int pc = abs(p - c); 2550 2551 // return nearest of a,b,c, 2552 // breaking ties in order a,b,c. 2553 if (pa <= pb && pa <= pc) return a; 2554 else if (pb <= pc) return b; 2555 else return c; 2556 } 2557 }